﻿using Microsoft.PlayerFramework;
using MullenStudio.Storage;
using System;
using System.Linq;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;

namespace MullenStudio.Media
{
    /// <summary>
    /// Play a list of video streams as if they're segments of a whole video
    /// </summary>
    public class ConcatInteractiveViewModel: InteractiveViewModel
    {
        public ConcatInteractiveViewModel() : base() { }    //  A dummy constructor, the only purpose is the let xmal know this class is a derived class of IIteractiveViewModel

        public ConcatInteractiveViewModel(MediaPlayer mediaPlayer)
            : base(mediaPlayer)
        {
            MediaPlayer.MarkerReached += MediaPlayer_MarkerReached;
            MediaPlayer.CurrentStateChanged += MediaPlayer_CurrentStateChanged;
            MediaPlayer.MediaEnding += MediaPlayer_MediaEnding;

            MediaPlayer.SeekWhileScrubbing = false; // Important!
        }

        #region Time Properties
        public override TimeSpan StartTime
        {
            get { return _endTimes == null ? base.StartTime : TimeSpan.Zero; }
        }

        public override TimeSpan EndTime
        {
            get { return _endTimes == null ? base.EndTime : _endTimes.Last(); }
        }

        public override TimeSpan Position
        {
            get { return _endTimes == null ? base.Position : (base.Position + _startTimes[CurrentSegment]); }
        }

        public override TimeSpan Duration
        {
            get { return _endTimes == null ? base.Duration : _endTimes.Last(); }
        }

        public override TimeSpan MaxPosition
        {
            get { return _endTimes == null ? base.MaxPosition : _endTimes.Last(); }
        }

        public override TimeSpan TimeRemaining
        {
            get { return _endTimes == null ? base.TimeRemaining : (_endTimes.Last() - (base.Position + _startTimes[CurrentSegment])); }
        }
        #endregion

        #region Seek Related Methods
        protected override void OnScrub(TimeSpan position, out bool canceled)
        {
            _lastSwitching = DateTime.MinValue;
            if (position >= _startTimes[_currentSegment] && position < _endTimes[_currentSegment])
            {
                _virtualCurrentSegment = null;
                base.OnScrub(position - _startTimes[_currentSegment], out canceled);
            }
            else if (position < TimeSpan.Zero || position >= _endTimes.Last())
            {
                _virtualCurrentSegment = null;
                canceled = true;
            }
            else
            {
                // SeekWhileScrubbing is false.
                // Therefore, Don't really seek nor swith the segment
                // Switch to new segment OnCompleteScrub
                // Reason:
                //   If SeekWhileScrubbing, when switching the segment, previous seek task is not completed.
                //   and will never get completed after switching the segment.
                //   Thus, all following seek tasks will not work properly.
                // However, still need to call base.OnScrub to set VirtualPosition
                _virtualCurrentSegment = _currentSegment;
                for (int i = 0; i < _segmentCount; i++)
                {
                    if (position >= _startTimes[i] && position < _endTimes[i])
                    {
                        _virtualCurrentSegment = i;
                        break;
                    }
                }
                base.OnScrub(position - _startTimes[_virtualCurrentSegment.Value], out canceled);
            }
        }

        protected override void OnCompleteScrub(TimeSpan position, ref bool canceled)
        {
            _lastSwitching = DateTime.MinValue;
            if (position >= _startTimes[_currentSegment] && position < _endTimes[_currentSegment])
                base.OnCompleteScrub(position - _startTimes[_currentSegment], ref canceled);
            else if (position < TimeSpan.Zero || position >= _endTimes.Last())
            {
                canceled = true;
                base.OnCompleteScrub(position, ref canceled);
            }
            else
            {
                // Switch to new segment
                SwitchSource(_virtualCurrentSegment.Value, position - _startTimes[_virtualCurrentSegment.Value]);
                _virtualCurrentSegment = null;
                canceled = true;
                base.OnCompleteScrub(position - _startTimes[_currentSegment], ref canceled);
            }
        }

        protected override void OnSeek(TimeSpan position, out bool canceled)
        {
            _lastSwitching = DateTime.MinValue;
            if (position >= _startTimes[_currentSegment] && position < _endTimes[_currentSegment])
                base.OnSeek(position - _startTimes[_currentSegment], out canceled);
            else if (position < TimeSpan.Zero || position >= _endTimes.Last())
                canceled = true;
            else
            {
                // Switch video segment here
                int newSegment = _currentSegment;
                for (int i = 0; i < _segmentCount; i++)
                {
                    if (position >= _startTimes[i] && position < _endTimes[i])
                    {
                        newSegment = i;
                        break;
                    }
                }
                SwitchSource(newSegment, position - _startTimes[newSegment]);
                canceled = false;
                //base.OnSeek(position - _startTimes[_currentSegment], out canceled);
            }
        }

        protected override void OnSkipBack(TimeSpan position)
        {
            _lastSwitching = DateTime.MinValue;
            if (position >= _startTimes[_currentSegment] && position < _endTimes[_currentSegment])
                base.OnSkipBack(position - _startTimes[_currentSegment]);
            else
            {
                // Switch video segment here
                int newSegment = _currentSegment;
                for (int i = 0; i < _segmentCount; i++)
                {
                    if (position >= _startTimes[i] && position < _endTimes[i])
                    {
                        newSegment = i;
                        break;
                    }
                }
                SwitchSource(newSegment, position - _startTimes[newSegment]);
            }
        }

        protected override void OnSkipAhead(TimeSpan position)
        {
            _lastSwitching = DateTime.MinValue;
            if (position >= _startTimes[_currentSegment] && position < _endTimes[_currentSegment])
                base.OnSkipAhead(position - _startTimes[_currentSegment]);
            else
            {
                // Switch video segment here
                int newSegment = _currentSegment;
                for (int i = 0; i < _segmentCount; i++)
                {
                    if (position >= _startTimes[i] && position < _endTimes[i])
                    {
                        newSegment = i;
                        break;
                    }
                }
                SwitchSource(newSegment, position - _startTimes[newSegment]);
            }
        }
        #endregion

        #region Play Related Fields and Properties
        IRandomAccessStream[] _segmentStreams;
        string[] _mimeTypes;
        TimeSpan[] _durations;
        TimeSpan[] _endTimes;
        TimeSpan[] _startTimes;
        int _segmentCount;
        int _currentSegment;
        int? _virtualCurrentSegment;

        /// <summary>
        /// The index of current playing segment
        /// </summary>
        /// <remarks>If the progress bar is scrubbing but not complete yet, this will give the virtual segement</remarks>
        public int CurrentSegment
        {
            get
            {
                if (_virtualCurrentSegment.HasValue)
                    return _virtualCurrentSegment.Value;
                else
                    return _currentSegment;
            }
        }

        /// <summary>
        /// Get the number of segments
        /// </summary>
        public int SegmentCount
        {
            get { return _segmentCount; }
        }
        #endregion

        #region MediaPlayer Eventhandlers
        bool _stopPlay = false;
        DateTime _lastSwitching = DateTime.MinValue;
        // The MediaPlayer_MediaEnding and MediaPlayer_MarkerReached may both happen, and one can happen even after another switched the source.
        // Therefore, it's hard to avoid the switch source happen twice.
        // The way to avoid that is by stop any following switching for SwitchingTimeThreshold length.
        TimeSpan SwitchingTimeThreshold = TimeSpan.FromSeconds(0.2);

        void MediaPlayer_MediaEnding(object sender, MediaPlayerDeferrableEventArgs e)
        {
            if (DateTime.Now - _lastSwitching < SwitchingTimeThreshold)
                return;
            int newSegment = _currentSegment;
            if (newSegment < _segmentCount - 1)
                newSegment++;
            else
            {
                newSegment = 0;
                // Prevent automatically restart from the beginning
                _stopPlay = true;

                if (MediaEnded != null)
                    MediaEnded(this, new RoutedEventArgs());
            }
            SwitchSource(newSegment, null);
        }

        void MediaPlayer_CurrentStateChanged(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            if (MediaPlayer.CurrentState == Windows.UI.Xaml.Media.MediaElementState.Playing)
            {
                // Reset StartupPosition
                MediaPlayer.StartupPosition = null;
                // Stop play (automatically restart) if the video already finished
                if (_stopPlay)
                {
                    MediaPlayer.Stop();
                    _stopPlay = false;
                }
            }
        }

        void MediaPlayer_MarkerReached(object sender, Windows.UI.Xaml.Media.TimelineMarkerRoutedEventArgs e)
        {
            if (DateTime.Now - _lastSwitching < SwitchingTimeThreshold)
                return;
            _lastSwitching = DateTime.Now;
            if (e.Marker.Type != "EndSegment")
                return;
            int newSegment = _currentSegment;
            if (newSegment < _segmentCount - 1)
                newSegment++;
            else
            {
                newSegment = 0;
                // Prevent automatically restart from the beginning
                _stopPlay = true;

                if (MediaEnded != null)
                    MediaEnded(this, new RoutedEventArgs());
            }
            SwitchSource(newSegment, null);
        }
        #endregion

        #region Segment Setup Methods
        /// <summary>
        /// Set segments for normal streams
        /// </summary>
        /// <param name="segmentStreams">The streams for each segment</param>
        /// <param name="mimeTypes">The mientypes for each segment</param>
        /// <param name="durations">The end times for each segment</param>
        /// <remarks>
        /// If the stream is a DownloadRandomAccessStream, if that is already completed, it will work normally.
        /// If it is already initialized, it will start downloading when that segment starts playing.
        /// If it is not initialized, it may skip that segment.
        /// </remarks>
        public void SetSegments(IRandomAccessStream[] segmentStreams, string[] mimeTypes, TimeSpan[] durations)
        {
            if (segmentStreams.Length == 0 || segmentStreams.Length != mimeTypes.Length || segmentStreams.Length != durations.Length)
                throw new ArgumentException();

            _segmentStreams = (IRandomAccessStream[])segmentStreams.Clone();
            _mimeTypes = (string[])mimeTypes.Clone();
            _durations = (TimeSpan[])durations.Clone();
            IsProgressiveDownload = false;

            SetSegmentCommon();

            Play();
        }

        private void SetSegmentCommon()
        {
            _segmentCount = _segmentStreams.Length;
            _startTimes = new TimeSpan[_segmentCount];
            _endTimes = new TimeSpan[_segmentCount];
            _startTimes[0] = TimeSpan.Zero;
            _endTimes[0] = _durations[0];
            for (int i = 1; i < _segmentCount; i++)
            {
                _startTimes[i] = _endTimes[i - 1];
                _endTimes[i] = _startTimes[i] + _durations[i];
            }
        }

        /// <summary>
        /// Play video from start
        /// </summary>
        protected void Play()
        {
            if (MediaPlayer.CurrentState == Windows.UI.Xaml.Media.MediaElementState.Playing)
                return;

            SwitchSource(0, null);
        }

        /// <summary>
        /// Set progressive download segments
        /// </summary>
        /// <param name="segmentStreams">The streams for each segment</param>
        /// <param name="mimeTypes">The minetypes for each segment</param>
        /// <param name="endTimes">The end times for each segment</param>
        /// <remarks>All the streams need to be initialized</remarks>
        /// <exception cref="ArgumentException">
        /// If the number of segments not meet, or if the stream is not initialized, or if there are same streams in the segmentStreams
        /// </exception>
        public void SetSegments(DownloadRandomAccessStream[] segmentStreams, string[] mimeTypes, TimeSpan[] durations)
        {
            if (segmentStreams.Length == 0 || segmentStreams.Length != mimeTypes.Length || segmentStreams.Length != durations.Length)
                throw new ArgumentException();

            for (int i = 0; i < segmentStreams.Length; i++)
            {
                if (!segmentStreams[i].IsInitialized)
                    throw new ArgumentException();
                for (int j = i + 1; j < segmentStreams.Length; j++)
                {
                    if (segmentStreams[i] == segmentStreams[j])
                        throw new ArgumentException();
                }
            }

            _segmentStreams = (IRandomAccessStream[])segmentStreams.Clone();
            _mimeTypes = (string[])mimeTypes.Clone();
            _durations = (TimeSpan[])durations.Clone();
            IsProgressiveDownload = true;

            SetSegmentCommon();

            Play();
        }
        #endregion

        #region SwitchSource Helper
        /// <summary>
        /// Switch playing segment
        /// </summary>
        /// <param name="newSegment">Index of new segment</param>
        /// <param name="startupPosition">Optional startup position</param>
        protected void SwitchSource(int newSegment, TimeSpan? startupPosition = null)
        {
            SetupDownload(newSegment);
            _currentSegment = newSegment;
            MediaPlayer.StartupPosition = startupPosition;
            MediaPlayer.Markers.Clear();
            MediaPlayer.SetSource(_segmentStreams[_currentSegment], _mimeTypes[_currentSegment]);
            MediaPlayer.Markers.Add(new TimelineMarker() { Time = _durations[_currentSegment], Type = "EndSegment" });
        }
        #endregion

        #region New Media Events
        public event EventHandler<RoutedEventArgs> MediaEnded;
        #endregion

        #region Download Related Methods
        /// <summary>
        /// Get if currently dealing with progressive download
        /// </summary>
        public bool IsProgressiveDownload { get; protected set; }

        int _currentDownloadStream = -1;

        /// <summary>
        /// Setep download for a new segment
        /// </summary>
        /// <param name="newSegment">The index of the new segment. If the value is -1, it will be the next segment.</param>
        protected void SetupDownload(int newSegment = -1)
        {
            if (IsProgressiveDownload)
            {
                if (newSegment == -1)
                {
                    if (_currentDownloadStream < _segmentCount - 1)
                        newSegment = _currentDownloadStream + 1;
                    else
                        newSegment = 0;
                }
                int toDownload = -1;
                DownloadRandomAccessStream dras = null;
                // Find the next segment that not finished yet from the newSegment
                for (int i = 0; i < _segmentCount; i++)
                {
                    dras = _segmentStreams[(newSegment + i) % _segmentCount] as DownloadRandomAccessStream;
                    if (dras.BytesReceived < dras.TotalBytesToReceive)
                    {
                        // Not finished yet, this is the stream to download next
                        toDownload = (newSegment + i) % _segmentCount;
                        break;
                    }
                }
                
                if (toDownload == -1)   // Nothing to download, all completed
                    return;
                if (toDownload == _currentDownloadStream)   // The stream to download is same as current stream
                {
                    // Download from start
                    (_segmentStreams[toDownload] as DownloadRandomAccessStream).Seek(0);
                    return;
                }

                if (_currentDownloadStream >= 0)
                {
                    // Stop previous download
                    dras = _segmentStreams[_currentDownloadStream] as DownloadRandomAccessStream;
                    dras.Pause();
                    dras.DownloadFinished -= DownloadRandomAccessStream_DownloadFinished;
                    dras.DataReceived -= DownloadRandomAccessStream_DataReceived;
                }
                // Start new download
                _currentDownloadStream = toDownload;
                dras = _segmentStreams[_currentDownloadStream] as DownloadRandomAccessStream;
                dras.DownloadFinished += DownloadRandomAccessStream_DownloadFinished;
                dras.DataReceived += DownloadRandomAccessStream_DataReceived;
                dras.Seek(0);
            }
        }

        void DownloadRandomAccessStream_DownloadFinished(object sender, EventArgs e)
        {
            SetupDownload();
        }

        async void DownloadRandomAccessStream_DataReceived(object sender, EventArgs e)
        {
            if (_currentDownloadStream >= _currentSegment)
                // Make sure it runs on the correct thread
                await MediaPlayer.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                    OnPropertyChanged("DownloadProgress"));
        }

        public override double DownloadProgress
        {
            get
            {
                if (IsProgressiveDownload)
                {
                    double progress = 1;
                    DownloadRandomAccessStream dras = _segmentStreams[_currentSegment] as DownloadRandomAccessStream;
                    if (dras.CurrentReadingBlockEndPosition < dras.Size)
                        progress = (((double)dras.CurrentReadingBlockEndPosition / dras.Size) * _durations[_currentSegment].TotalMilliseconds + 
                            _startTimes[_currentSegment].TotalMilliseconds) / _endTimes.Last().TotalMilliseconds;
                    else
                    {
                        for (int i = _currentSegment + 1; i < _segmentCount; i++)
                        {
                            dras = _segmentStreams[i] as DownloadRandomAccessStream;
                            if (dras.FirstBlockEndPosition < dras.Size)
                            {
                                progress = (((double)dras.FirstBlockEndPosition / dras.Size) * _durations[i].TotalMilliseconds +
                                    _startTimes[i].TotalMilliseconds) / _endTimes.Last().TotalMilliseconds;
                                break;
                            }
                        }
                    }

                    // Just in case
                    if (progress < 0)
                        progress = 0;
                    else if (progress > 1)
                        progress = 1;

                    return progress;
                }
                else
                    return base.DownloadProgress;
            }
        }
        #endregion
    }
}
